package cn.xiongxf.service.impl;

import cn.xiongxf.constant.RedisKeyConstant;
import cn.xiongxf.domain.KillActivity;
import cn.xiongxf.domain.KillCourse;
import cn.xiongxf.dto.KillParamDTO;
import cn.xiongxf.dto.PreCourseOrder2RedisDTO;
import cn.xiongxf.mapper.KillCourseMapper;
import cn.xiongxf.service.IKillActivityService;
import cn.xiongxf.service.IKillCourseService;
import cn.xiongxf.util.AssertUtil;
import cn.xiongxf.util.CodeGenerateUtils;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.redisson.api.RLock;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author xiongxiaofu
 * @since 2022-09-01
 */
@Service
@Slf4j
public class KillCourseServiceImpl extends ServiceImpl<KillCourseMapper, KillCourse> implements IKillCourseService {
    @Autowired
    private IKillActivityService activityService;
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RocketMQTemplate mqTemplate;

    /**
     * 添加秒杀课程
     */
    @Override
    public void add(KillCourse killCourse) {
        KillCourse killCourseDB = selectOne(new EntityWrapper<KillCourse>()
                        .eq("activity_id", killCourse.getActivityId())
                        .eq("course_id", killCourse.getCourseId()));
        AssertUtil.isNull(killCourseDB, "该课程已加入所选秒杀活动了！");

        KillActivity killActivity = activityService.selectById(killCourse.getActivityId());
        BeanUtils.copyProperties(killActivity, killCourse);
        killCourse.setCreateTime(new Date());

        insert(killCourse);
    }

    /**
     * 查询所有发布的秒杀课程
     */
    @Override
    public List<KillCourse> onlineAll() {
//        List killCourses = redisTemplate.opsForHash().values("killCourses");  // 单个对象能强转，集合不能强转
        // 换一种API：opsForHash() -> boundGeoOps()
        BoundHashOperations<Object, Object, KillCourse> operations = redisTemplate.boundHashOps(RedisKeyConstant.KILL_COURSES);
        return operations.values();
    }

    /**
     * 查询单个发布的秒杀课程
     */
    @Override
    public KillCourse onlineOne(Long killId) {
        AssertUtil.isNotNull(killId, "夰法请求！");
        return (KillCourse)redisTemplate.opsForHash().get(RedisKeyConstant.KILL_COURSES, killId.toString());
    }

    /**
     * 执行秒杀
     * 接口优化：
     * 1.算法
     * 2.jvm优化(垃圾回收算法，内存大小)
     * 3.Tomcat优化/线程池
     * 4.数据库优化：缓存替代数据库
     * 5.网络优化
     * 6.硬件
     * 7.异步 MQ
     * 8.集群
     */
    @Override
    public String kill(KillParamDTO dto) {
        // 1.参数校验

        // 2.条件校验：
        // 2.1.是否登录  // todo
        long loginId = 9L;
        // 2.2.不能重复秒杀
        String killLog =  "order:" + loginId + ":" + dto.getKillCourseId();
//       AssertUtil.isFalse(redisTemplate.hasKey(killLog), "不能重复秒杀！");


        // 2.3.是否处于秒杀中
        KillCourse killCourse = onlineOne(dto.getKillCourseId());
        AssertUtil.isNotNull(killCourse, "秒杀商品无效！");
        AssertUtil.isTrue(killCourse.isKilling(), "秒杀时间非法！");
        // 2.4.秒杀码
        AssertUtil.isEquals(dto.getCode(), killCourse.getCode(), "非法请求！");

        // 3.执行秒杀：扣减信号量（）
        RSemaphore semaphore = redissonClient.getSemaphore(RedisKeyConstant.KILL_COURSE_STORE + killCourse.getId());
        int killCount = 100;
        boolean killSuccess = semaphore.tryAcquire(killCount);
        AssertUtil.isTrue(killSuccess, "秒杀失败！");


        // 4.预创订单
        PreCourseOrder2RedisDTO order2Redis = new PreCourseOrder2RedisDTO();

        String orderNo = CodeGenerateUtils.generateOrderSn(loginId);
        order2Redis.setAmount(killCourse.getKillPrice());
        order2Redis.setCount(killCount);
        order2Redis.setCourseId(killCourse.getCourseId());
        order2Redis.setCourseName(killCourse.getCourseName());
        order2Redis.setCoursePic(killCourse.getCoursePic());
        order2Redis.setOrderNo(orderNo);
        order2Redis.setKillCourseId(killCourse.getId());
        order2Redis.setUserId(loginId);

        // 5.预创单存入Redis
        String preOrderKey = "order:" + loginId + ":" + orderNo;
        redisTemplate.opsForValue().set(preOrderKey, order2Redis);

        // 保存秒杀记录，用于判断重复杪杀
        redisTemplate.opsForValue().set(killLog, "kill-log");

        log.info("秒杀耗时：{}", System.currentTimeMillis() - killCourse.getStartTime().getTime());

        // 【秒杀下单超时处理】：发送MQ延迟消息
        Message<String> message = MessageBuilder.withPayload(preOrderKey).build();
        SendResult sendResult = mqTemplate.syncSend("topic-kill-expire:tags-kill-expire", message, 2000, 3);
        log.info("发送延迟消息：{}", sendResult);

        // 6.返回订单号
        return orderNo;
    }

    /**
     * 秒杀下单超时处理
     */
    @Override
    public void cancelPreOrder(String preOrderKey) {
        // 加分布式锁 - 解决退库存和下单同时成功问题
        RLock lock = redissonClient.getLock("lock" + preOrderKey);  // Redis中已经有preOrderKey这个key了
        try {
            lock.lock();
            log.info("加锁成功 =============================================");
            PreCourseOrder2RedisDTO preOrder = (PreCourseOrder2RedisDTO) redisTemplate.opsForValue().get(preOrderKey);
            if (preOrder == null){
                log.info("检查下单超时：预创单不存在，可能已下单");
                return;
            }
            // 下单超时：
            // 退库存
            RSemaphore semaphore = redissonClient.getSemaphore(RedisKeyConstant.KILL_COURSE_STORE + preOrder.getKillCourseId());
            semaphore.release(preOrder.getCount());
            // 删除预创单
            redisTemplate.delete(preOrderKey);
            log.info("下单超时：退库存，删除预创单：{}", preOrder);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (lock.isLocked()) lock.unlock();
            log.info("释放锁成功 =============================================");
        }
    }

}
